#region license
/*
 * Copyright (C) 2007 CchenSoft.com
 * 
 * This library is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Lesser General Public License 2.1 or later, as
 * published by the Free Software Foundation. See the included License.txt
 * or http://www.gnu.org/copyleft/lesser.html for details.
 * 
 * Author: Billy Zhang
 */
#endregion

using System.IO;
using System;
using System.Collections;
using System.Xml;
using System.Collections.Generic;
using CchenSoft.Workflow.Basic;

namespace CchenSoft.Workflow.Loader
{
    public class WorkflowDescriptor : AbstractDescriptor, IValidatable
    {
        //~ Static fields/initializers /////////////////////////////////////////////

        public const string XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
        public const string DOCTYPE_DECL = "<!DOCTYPE workflow PUBLIC \"-//OpenSymphony Group//DTD OSWorkflow 2.8//EN\" \"http://www.opensymphony.com/osworkflow/workflow_2_8.dtd\">";
        public const string DOCTYPE_NAME = "workflow";
        public const string DOCTYPE_PUB_ID = "\"-//OpenSymphony Group//DTD OSWorkflow 2.8//EN\"";
        public const string DOCTYPE_SYS_ID = "\"http://www.opensymphony.com/osworkflow/workflow_2_8.dtd\"";

        //~ Instance fields ////////////////////////////////////////////////////////

        protected ConditionsDescriptor globalConditions = null;
        protected List<ActionDescriptor> commonActionsList = new List<ActionDescriptor>(); // for preserving order
        protected List<ActionDescriptor> globalActions = new List<ActionDescriptor>();
        protected List<ActionDescriptor> initialActions = new List<ActionDescriptor>();
        protected List<JoinDescriptor> joins = new List<JoinDescriptor>();
        protected List<RegisterDescriptor> registers = new List<RegisterDescriptor>();
        protected List<SplitDescriptor> splits = new List<SplitDescriptor>();
        protected List<StepDescriptor> steps = new List<StepDescriptor>();
        protected Dictionary<int, ActionDescriptor> commonActions = new Dictionary<int, ActionDescriptor>();
        protected Dictionary<string, string> metaAttributes = new Dictionary<string, string>();
        protected Dictionary<int, FunctionDescriptor> timerFunctions = new Dictionary<int, FunctionDescriptor>();
        protected string workflowName = null;
        protected Type workflowType = null;

        //~ Constructors ///////////////////////////////////////////////////////////

        /**
         * @deprecated use {@link DescriptorFactory} instead
         */
        public WorkflowDescriptor()
        {
        }

        /**
         * @deprecated use {@link DescriptorFactory} instead
         */
        public WorkflowDescriptor(XmlElement root)
        {
            Init(root);
        }

        //~ Methods ////////////////////////////////////////////////////////////////

        public ActionDescriptor GetAction(int id)
        {
            // check global actions
            foreach (ActionDescriptor actionDescriptor in globalActions)
            {
                if (actionDescriptor.Id == id)
                {
                    return actionDescriptor;
                }
            }

            // check steps
            foreach (StepDescriptor stepDescriptor in steps)
            {
                ActionDescriptor actionDescriptor = stepDescriptor.GetAction(id);

                if (actionDescriptor != null)
                {
                    return actionDescriptor;
                }
            }

            //check initial actions, which we now must have unique id's
            foreach (ActionDescriptor actionDescriptor in initialActions)
            {
                if (actionDescriptor.Id == id)
                {
                    return actionDescriptor;
                }
            }

            return null;
        }

        /**
         * Get a Map of the common actions specified, keyed on actionId (an Integer)
         * @return A list of {@link ActionDescriptor} objects
         */
        public IDictionary<int, ActionDescriptor> CommonActions
        {
            get { return commonActions; }
        }

        /**
         * Get a List of the global actions specified
         * @return A list of {@link ActionDescriptor} objects
         */
        public IList<ActionDescriptor> GlobalActions
        {
            get { return globalActions; }
        }

        public ConditionsDescriptor GlobalConditions
        {
            get { return globalConditions; }
        }

        public ActionDescriptor GetInitialAction(int id)
        {
            foreach (ActionDescriptor actionDescriptor in initialActions)
            {
                if (actionDescriptor.Id == id)
                {
                    return actionDescriptor;
                }
            }

            return null;
        }

        /**
         * Get a List of initial steps for this workflow
         * @return A list of {@link ActionDescriptor} objects
         */
        public IList<ActionDescriptor> InitialActions
        {
            get { return initialActions; }
        }

        public JoinDescriptor GetJoin(int id)
        {
            foreach (JoinDescriptor joinDescriptor in joins)
            {
                if (joinDescriptor.Id == id)
                {
                    return joinDescriptor;
                }
            }

            return null;
        }

        /**
         * Get a List of initial steps for this workflow
         * @return A list of {@link JoinDescriptor} objects
         */
        public IList<JoinDescriptor> Joins
        {
            get { return joins; }
        }

        public IDictionary<string, string> MetaAttributes
        {
            get { return metaAttributes; }
        }

        public string Name
        {
            get { return workflowName; }
            set { workflowName = value; }
        }

        public Type WorkflowType
        {
            get { return workflowType; }
        }

        public IList<RegisterDescriptor> Registers
        {
            get { return registers; }
        }

        public SplitDescriptor GetSplit(int id)
        {
            foreach (SplitDescriptor splitDescriptor in splits)
            {
                if (splitDescriptor.Id == id)
                {
                    return splitDescriptor;
                }
            }

            return null;
        }

        /**
         * Get a List of initial steps for this workflow
         * @return A list of {@link SplitDescriptor} objects
         */
        public IList<SplitDescriptor> Splits
        {
            get { return splits; }
        }

        public StepDescriptor GetStep(int id)
        {
            foreach (StepDescriptor step in steps)
            {
                if (step.Id == id)
                {
                    return step;
                }
            }

            return null;
        }

        /**
         * Get a List of steps in this workflow
         * @return a List of {@link StepDescriptor} objects
         */
        public IList<StepDescriptor> Steps
        {
            get { return steps; }
        }

        /**
         * Update a trigger function
         * @param id The id for the trigger function
         * @param descriptor The descriptor for the trigger function
         * @return The old trigger function with the specified ID, if any existed
         */
        public FunctionDescriptor SetTriggerFunction(int id, FunctionDescriptor descriptor)
        {
            timerFunctions[id] = descriptor;
            return descriptor;
        }

        public FunctionDescriptor GetTriggerFunction(int id)
        {
            return (FunctionDescriptor)this.timerFunctions[id];
        }

        /**
         * Get a Map of all trigger functions in this workflow
         * @return a Map with Integer keys and {@link FunctionDescriptor} values
         */
        public IDictionary<int, FunctionDescriptor> TriggerFunctions
        {
            get { return timerFunctions; }
        }

        /**
         * Add a common action
         * @param descriptor The action descriptor to add
         * @throws IllegalArgumentException if the descriptor's ID already exists in the workflow
         */
        public void AddCommonAction(ActionDescriptor descriptor)
        {
            descriptor.IsCommon = true;
            AddAction(commonActions, descriptor);
            AddAction(commonActionsList, descriptor);
        }

        /**
         * Add a global action
         * @param descriptor The action descriptor to add
         * @throws IllegalArgumentException if the descriptor's ID already exists in the workflow
         */
        public void AddGlobalAction(ActionDescriptor descriptor)
        {
            AddAction(globalActions, descriptor);
        }

        /**
         * Add an initial action
         * @param descriptor The action descriptor to add
         * @throws IllegalArgumentException if the descriptor's ID already exists in the workflow
         */
        public void AddInitialAction(ActionDescriptor descriptor)
        {
            AddAction(initialActions, descriptor);
        }

        /**
         * Add a join
         * @param descriptor The join descriptor to add
         * @throws IllegalArgumentException if the descriptor's ID already exists in the workflow
         */
        public void AddJoin(JoinDescriptor descriptor)
        {
            if (GetJoin(descriptor.Id) != null)
            {
                throw new ArgumentException("Join with id " + descriptor.Id + " already exists");
            }

            joins.Add(descriptor);
        }

        /**
         * Add a split
         * @param descriptor The split descriptor to add
         * @throws IllegalArgumentException if the descriptor's ID already exists in the workflow
         */
        public void AddSplit(SplitDescriptor descriptor)
        {
            if (GetSplit(descriptor.Id) != null)
            {
                throw new ArgumentException("Split with id " + descriptor.Id + " already exists");
            }

            splits.Add(descriptor);
        }

        /**
         * Add a step
         * @param descriptor The step descriptor to add
         * @throws IllegalArgumentException if the descriptor's ID already exists in the workflow
         */
        public void AddStep(StepDescriptor descriptor)
        {
            if (GetStep(descriptor.Id) != null)
            {
                throw new ArgumentException("Step with id " + descriptor.Id + " already exists");
            }

            steps.Add(descriptor);
        }

        /**
         * Remove an action from this workflow completely.
         * <p>
         * This method will check global actions and all steps.
         *
         * @return true if the action was successfully removed, false if it was not found
         */
        public bool RemoveAction(ActionDescriptor actionToRemove)
        {
            // global actions
            for (IEnumerator iterator = globalActions.GetEnumerator();
                    iterator.MoveNext(); )
            {
                ActionDescriptor actionDescriptor = (ActionDescriptor)iterator.Current;

                if (actionDescriptor.Id == actionToRemove.Id)
                {
                    globalActions.Remove(actionDescriptor);

                    return true;
                }
            }

            // steps
            for (IEnumerator iterator = steps.GetEnumerator(); iterator.MoveNext(); )
            {
                StepDescriptor stepDescriptor = (StepDescriptor)iterator.Current;
                ActionDescriptor actionDescriptor = stepDescriptor.GetAction(actionToRemove.Id);

                if (actionDescriptor != null)
                {
                    stepDescriptor.Actions.Remove(actionDescriptor);

                    return true;
                }
            }

            return false;
        }

        public void Validate()
        {
            ValidationHelper.Validate((ICollection<IValidatable>)this.registers);
            ValidationHelper.Validate((ICollection<IValidatable>)this.TriggerFunctions.Values);
            ValidationHelper.Validate((ICollection<IValidatable>)this.globalActions);
            ValidationHelper.Validate((ICollection<IValidatable>)this.initialActions);
            ValidationHelper.Validate((ICollection<IValidatable>)this.CommonActions.Values);
            ValidationHelper.Validate((ICollection<IValidatable>)this.steps);
            ValidationHelper.Validate((ICollection<IValidatable>)this.splits);
            ValidationHelper.Validate((ICollection<IValidatable>)this.joins);

            List<int> actions = new List<int>();

            foreach (ActionDescriptor action in GlobalActions)
            {
                actions.Add(action.Id);
            }

            foreach (StepDescriptor step in steps)
            {
                foreach (ActionDescriptor action in step.Actions)
                {
                    // check to see if it's a common action (dups are ok, since that's the point of common actions!)
                    if (!action.IsCommon)
                    {
                        if (actions.Contains(action.Id))
                        {
                            throw new InvalidWorkflowDescriptorException("Duplicate occurance of action ID " + action.Id + " found in step " + step.Id);
                        }
                        else
                        {
                            actions.Add(action.Id);
                        }
                    }
                }
            }

            //now we have all our unique actions, let's check that no common action id's exist in them
            foreach (int action in commonActions.Keys)
            {
                if (actions.Contains(action))
                {
                    throw new InvalidWorkflowDescriptorException("common-action ID " + action + " is duplicated in a step action");
                }
            }

            foreach (ActionDescriptor action in initialActions)
            {
                if (actions.Contains(action.Id))
                {
                    throw new InvalidWorkflowDescriptorException("initial-action ID " + action + " is duplicated in a step action");
                }
            }

            ValidateDTD();
        }

        public override void WriteXML(XmlTextWriter writer, int indent)
        {
            //XMLUtil.printIndent(writer, indent++);
            writer.WriteStartElement("workflow");

            foreach (string key in metaAttributes.Keys)
            {
                //XMLUtil.printIndent(writer, indent);
                writer.WriteStartElement("meta");
                writer.WriteAttributeString("name", XMLUtil.Encode(key));
                writer.WriteString(XMLUtil.Encode(metaAttributes[key]));
                writer.WriteEndElement();
            }

            if (registers.Count > 0)
            {
                //XMLUtil.printIndent(writer, indent++);
                writer.WriteStartElement("registers");

                for (int i = 0; i < registers.Count; i++)
                {
                    RegisterDescriptor register = (RegisterDescriptor)registers[i];
                    register.WriteXML(writer, indent);
                }

                //XMLUtil.printIndent(writer, --indent);
                writer.WriteEndElement();
            }

            if (timerFunctions.Count > 0)
            {
                //XMLUtil.printIndent(writer, indent++);
                writer.WriteStartElement("trigger-functions");

                foreach (int key in timerFunctions.Keys)
                {
                    //XMLUtil.printIndent(writer, indent++);
                    writer.WriteStartElement("trigger-function");
                    writer.WriteAttributeString("id", XMLUtil.Encode(key));

                    FunctionDescriptor trigger = (FunctionDescriptor)timerFunctions[key];
                    trigger.WriteXML(writer, indent);
                    //XMLUtil.printIndent(writer, --indent);
                    writer.WriteEndElement(); //("</trigger-function>");
                }

                //while (iterator.hasNext()) {
                //}

                XMLUtil.printIndent(writer, --indent);
                writer.WriteEndElement(); // ("</trigger-functions>");
            }

            if (globalConditions != null)
            {
                //XMLUtil.printIndent(writer, indent++);
                writer.WriteStartElement("global-conditions");

                globalConditions.WriteXML(writer, indent);

                writer.WriteEndElement(); // ("</global-conditions>");
            }

            //XMLUtil.printIndent(writer, indent++);
            writer.WriteStartElement("initial-actions");

            for (int i = 0; i < initialActions.Count; i++)
            {
                ActionDescriptor action = (ActionDescriptor)initialActions[i];
                action.WriteXML(writer, indent);
            }

            //XMLUtil.printIndent(writer, --indent);
            writer.WriteEndElement(); // ("</initial-actions>");

            if (globalActions.Count > 0)
            {
                //XMLUtil.printIndent(writer, indent++);
                writer.WriteStartElement("global-actions");

                for (int i = 0; i < globalActions.Count; i++)
                {
                    ActionDescriptor action = (ActionDescriptor)globalActions[i];
                    action.WriteXML(writer, indent);
                }

                //XMLUtil.printIndent(writer, --indent);
                writer.WriteEndElement(); // ("</global-actions>");
            }

            if (commonActions.Count > 0)
            {
                //XMLUtil.printIndent(writer, indent++);
                writer.WriteStartElement("common-actions");

                foreach (ActionDescriptor action in commonActions.Values)
                {
                    action.WriteXML(writer, indent);
                }

                XMLUtil.printIndent(writer, --indent);
                writer.WriteEndElement(); // ("</common-actions>");
            }

            //XMLUtil.printIndent(writer, indent++);
            writer.WriteStartElement("steps");

            for (int i = 0; i < steps.Count; i++)
            {
                StepDescriptor step = (StepDescriptor)steps[i];
                step.WriteXML(writer, indent);
            }

            //XMLUtil.printIndent(writer, --indent);
            writer.WriteEndElement(); // ("</steps>");

            if (splits.Count > 0)
            {
                //XMLUtil.printIndent(writer, indent++);
                writer.WriteStartElement("splits");

                for (int i = 0; i < splits.Count; i++)
                {
                    SplitDescriptor split = (SplitDescriptor)splits[i];
                    split.WriteXML(writer, indent);
                }

                //XMLUtil.printIndent(writer, --indent);
                writer.WriteEndElement(); // ("</splits>");
            }

            if (joins.Count > 0)
            {
                //XMLUtil.printIndent(writer, indent++);
                writer.WriteStartElement("joins");

                for (int i = 0; i < joins.Count; i++)
                {
                    JoinDescriptor join = (JoinDescriptor)joins[i];
                    join.WriteXML(writer, indent);
                }

                //XMLUtil.printIndent(writer, --indent);
                writer.WriteEndElement(); // ("</joins>");
            }

            //XMLUtil.printIndent(writer, --indent);
            writer.WriteEndElement(); // ("</workflow>");
        }

        protected void Init(XmlElement root)
        {
            XmlNodeList children = root.ChildNodes;

            XmlNodeList metas = root.SelectNodes("meta");
            for (int i = 0; i < metas.Count; i++)
            {
                XmlElement meta = (XmlElement)metas[i];
                this.metaAttributes.Add(XMLUtil.GetAttributeValue(meta, "name"), meta.InnerText);
            }

            if (metaAttributes.ContainsKey("type"))
            {
                workflowType = Type.GetType(metaAttributes["type"]);
            }

            if (workflowType == null)
            {
                workflowType = typeof(BasicWorkflow);
                metaAttributes["type"] = workflowType.Name;
            }

            // handle registers - OPTIONAL
            XmlNode r = root.SelectSingleNode("registers");

            if (r != null)
            {
                XmlNodeList registers = r.SelectNodes("register");

                for (int i = 0; i < registers.Count; i++)
                {
                    XmlElement register = (XmlElement)registers[i];
                    RegisterDescriptor registerDescriptor = DescriptorFactory.GetFactory().CreateRegisterDescriptor(register);
                    registerDescriptor.Parent = this;
                    this.registers.Add(registerDescriptor);
                }
            }

            // handle global-conditions - OPTIONAL
            XmlNode globalConditionsElement = root.SelectSingleNode("global-conditions");

            if (globalConditionsElement != null)
            {
                XmlElement globalConditions = (XmlElement)globalConditionsElement.SelectSingleNode("conditions");

                ConditionsDescriptor conditionsDescriptor = DescriptorFactory.GetFactory().CreateConditionsDescriptor(globalConditions);
                conditionsDescriptor.Parent = this;
                this.globalConditions = conditionsDescriptor;
            }

            // handle initial-steps - REQUIRED
            XmlElement intialActionsElement = (XmlElement)root.SelectSingleNode("initial-actions");
            XmlNodeList initialActions = intialActionsElement.SelectNodes("action");

            for (int i = 0; i < initialActions.Count; i++)
            {
                XmlElement initialAction = (XmlElement)initialActions[i];
                ActionDescriptor actionDescriptor = DescriptorFactory.GetFactory().CreateActionDescriptor(initialAction);
                actionDescriptor.Parent = this;
                this.initialActions.Add(actionDescriptor);
            }

            // handle global-actions - OPTIONAL
            XmlNode globalActionsElement = root.SelectSingleNode("global-actions");

            if (globalActionsElement != null)
            {
                XmlNodeList globalActions = globalActionsElement.SelectNodes("action");

                for (int i = 0; i < globalActions.Count; i++)
                {
                    XmlElement globalAction = (XmlElement)globalActions[i];
                    ActionDescriptor actionDescriptor = DescriptorFactory.GetFactory().CreateActionDescriptor(globalAction);
                    actionDescriptor.Parent = this;
                    this.globalActions.Add(actionDescriptor);
                }
            }

            // handle common-actions - OPTIONAL
            //   - Store actions in HashMap for now. When parsing Steps, we'll resolve
            //      any common actions into local references.
            XmlNode commonActionsElement = root.SelectSingleNode("common-actions");

            if (commonActionsElement != null)
            {
                XmlNodeList commonActions = commonActionsElement.SelectNodes("action");

                for (int i = 0; i < commonActions.Count; i++)
                {
                    XmlElement commonAction = (XmlElement)commonActions[i];
                    ActionDescriptor actionDescriptor = DescriptorFactory.GetFactory().CreateActionDescriptor(commonAction);
                    actionDescriptor.Parent = this;
                    AddCommonAction(actionDescriptor);
                }
            }

            // handle timer-functions - OPTIONAL
            XmlNode timerFunctionsElement = root.SelectSingleNode("trigger-functions");

            if (timerFunctionsElement != null)
            {
                XmlNodeList timerFunctions = timerFunctionsElement.SelectNodes("trigger-function");

                for (int i = 0; i < timerFunctions.Count; i++)
                {
                    XmlElement timerFunction = (XmlElement)timerFunctions[i];
                    int id = Convert.ToInt32(XMLUtil.GetAttributeValue(timerFunction, "id"));
                    FunctionDescriptor function = DescriptorFactory.GetFactory().CreateFunctionDescriptor((XmlElement)timerFunction.SelectSingleNode("function"));
                    function.Parent = this;
                    this.timerFunctions.Add(id, function);
                }
            }

            // handle steps - REQUIRED
            XmlElement stepsElement = (XmlElement)root.SelectSingleNode("steps");
            XmlNodeList steps = stepsElement.SelectNodes("step");

            for (int i = 0; i < steps.Count; i++)
            {
                XmlElement step = (XmlElement)steps[i];
                StepDescriptor stepDescriptor = DescriptorFactory.GetFactory().CreateStepDescriptor(step, this);
                this.steps.Add(stepDescriptor);
            }

            // handle splits - OPTIONAL
            XmlNode splitsElement = root.SelectSingleNode("splits");

            if (splitsElement != null)
            {
                XmlNodeList split = splitsElement.SelectNodes("split");

                for (int i = 0; i < split.Count; i++)
                {
                    XmlElement s = (XmlElement)split[i];
                    SplitDescriptor splitDescriptor = DescriptorFactory.GetFactory().CreateSplitDescriptor(s);
                    splitDescriptor.Parent = this;
                    this.splits.Add(splitDescriptor);
                }
            }

            // handle joins - OPTIONAL:
            XmlNode joinsElement = root.SelectSingleNode("joins");

            if (joinsElement != null)
            {
                XmlNodeList join = joinsElement.SelectNodes("join");

                for (int i = 0; i < join.Count; i++)
                {
                    XmlElement s = (XmlElement)join[i];
                    JoinDescriptor joinDescriptor = DescriptorFactory.GetFactory().CreateJoinDescriptor(s);
                    joinDescriptor.Parent = this;
                    this.joins.Add(joinDescriptor);
                }
            }
        }

        // refactored this out from the three addAction methods above
        private void AddAction(Object actionsCollectionOrMap, ActionDescriptor descriptor)
        {
            if (GetAction(descriptor.Id) != null)
            {
                throw new ArgumentException("action with id " + descriptor.Id + " already exists for this step.");
            }

            if (actionsCollectionOrMap is IDictionary)
            {
                ((IDictionary)actionsCollectionOrMap).Add(descriptor.Id, descriptor);
            }
            else
            {
                ((ArrayList)actionsCollectionOrMap).Add(descriptor);
            }
        }

        private void ValidateDTD()
        {
            StringWriter sw = new StringWriter();
            XmlTextWriter writer = new XmlTextWriter(sw);
            writer.WriteStartDocument();
            writer.WriteDocType(DOCTYPE_NAME, DOCTYPE_PUB_ID, DOCTYPE_SYS_ID, "");
            WriteXML(writer, 0);

            try
            {

                XmlDocument doc = new XmlDocument();
                doc.LoadXml(sw.ToString());

                //doc.Validate();
            }
            catch (InvalidWorkflowDescriptorException e)
            {
                throw e;
            }
            catch (Exception e)
            {
                throw new InvalidWorkflowDescriptorException(e.ToString());
            }
        }
    }
}
